{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 构建卷积神经网络\n",
    "- 卷积网络中的输入和层与传统神经网络有些区别，需重新设计，训练模块基本一致"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-04-09T14:19:23.602054400Z",
     "start_time": "2024-04-09T14:19:23.600074800Z"
    }
   },
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "import torch.utils.data\n",
    "import torch.nn.functional as F\n",
    "from torchvision import datasets, transforms\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 1.首先读取数据\n",
    "- 分别构建训练集和测试集（验证集）\n",
    "- DataLoader来迭代取数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2024-04-09T14:19:27.082432500Z",
     "start_time": "2024-04-09T14:19:27.043555100Z"
    }
   },
   "outputs": [],
   "source": [
    "# 定义超参数 \n",
    "input_size = 28  # 图像的总尺寸28*28\n",
    "num_classes = 10  # 标签的种类数\n",
    "num_epochs = 3  # 训练的总循环周期\n",
    "batch_size = 64  # 一个撮（批次）的大小，64张图片\n",
    "\n",
    "# 训练集\n",
    "train_dataset = datasets.MNIST(root='./data',\n",
    "                               train=True,  # 表明是训练集\n",
    "                               transform=transforms.ToTensor(),  # 图像转为tensor类型\n",
    "                               download=True  # 自动下载数据集到root目录，已下载则不会下载\n",
    "                               )\n",
    "\n",
    "# 测试集\n",
    "test_dataset = datasets.MNIST(root='./data',\n",
    "                              train=False,  # 表明是测试集\n",
    "                              transform=transforms.ToTensor()  # 图像转为tensor类型\n",
    "                              )\n",
    "\n",
    "# 构建batch数据\n",
    "train_loader = torch.utils.data.DataLoader(dataset=train_dataset,\n",
    "                                           batch_size=batch_size,\n",
    "                                           shuffle=True\n",
    "                                           )\n",
    "test_loader = torch.utils.data.DataLoader(dataset=test_dataset,\n",
    "                                          batch_size=batch_size,\n",
    "                                          shuffle=True\n",
    "                                          )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 卷积网络模块构建\n",
    "- 一般卷积层，relu层，池化层可以写成一个套餐\n",
    "- 注意卷积最后结果还是一个特征图，需要把图转换成向量才能做分类或者回归任务"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2024-04-09T14:19:30.716590Z",
     "start_time": "2024-04-09T14:19:30.714331900Z"
    }
   },
   "outputs": [],
   "source": [
    "class CNN(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(CNN, self).__init__()\n",
    "        self.conv1 = nn.Sequential(  # 输入大小 (1, 28, 28)\n",
    "            nn.Conv2d(  # 2D的卷积做任务\n",
    "                in_channels=1,  # 灰度图(上一层的输入是几个channel，灰度图只有一个channel)\n",
    "                out_channels=16,  # 要得到几多少个特征图(卷积核个数)\n",
    "                kernel_size=5,  # 卷积核大小(5*5)\n",
    "                stride=1,  # 步长\n",
    "                padding=2,  # 如果希望卷积后大小跟原来一样，需要设置padding=(kernel_size-1)/2 if stride=1\n",
    "            ),  # 输出的特征图为 (16, 28, 28)\n",
    "            nn.ReLU(),  # relu层，\n",
    "            nn.MaxPool2d(kernel_size=2),  # 进行池化操作（2x2 区域）, 输出结果为： (16, 14, 14)\n",
    "        )\n",
    "        self.conv2 = nn.Sequential(  # 下一个套餐的输入 (16, 14, 14)\n",
    "            nn.Conv2d(16, 32, 5, 1, 2),  # 输出 (32, 14, 14)\n",
    "            nn.ReLU(),  # relu层\n",
    "            nn.Conv2d(32, 32, 5, 1, 2),  # 输出 (32, 14, 14)\n",
    "            nn.ReLU(),  # relu层\n",
    "            nn.MaxPool2d(2),  # 输出 (32, 7, 7)\n",
    "        )\n",
    "\n",
    "        self.conv3 = nn.Sequential(  # 下一个套餐的输入 (16, 14, 14)\n",
    "            nn.Conv2d(32, 64, 5, 1, 2),  # 输出 (64, 14, 14)\n",
    "            nn.ReLU(),  # 输出 (64, 7, 7)  (channel, H, W)\n",
    "        )\n",
    "\n",
    "        self.out = nn.Linear(64 * 7 * 7, 10)  # 全连接层得到的结果，想要对结果进行10分类，需要进行全连接，隐层大小[3136*10]\n",
    "\n",
    "    def forward(self, x):  # 前向传播\n",
    "        x = self.conv1(x)\n",
    "        x = self.conv2(x)\n",
    "        x = self.conv3(x)\n",
    "        # 此时 x是一个4维向量[batch * channel * H * W]，\n",
    "        x = x.view(x.size(0), -1)  # flatten操作，结果为：(batch_size, 32 * 7 * 7)\n",
    "        # 通过view自动计算之后变为[batch, channel * H * W]的二维矩阵->[batch, channel*7*7]->[batch, 3136]\n",
    "        # 特征图不能做全连接操作，转为二维矩阵之后才能进行后面的全连接层计算\n",
    "        output = self.out(x)\n",
    "        return output"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2.准确率作为评估标准"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2024-04-09T14:19:33.130287700Z",
     "start_time": "2024-04-09T14:19:33.126550700Z"
    }
   },
   "outputs": [],
   "source": [
    "def accuracy(predictions, labels):  # 预测值，真实值(标签值)\n",
    "    pred = torch.max(predictions.data, 1)[1]  # 返回的是(最大值, 最大值的索引)，所以我们只获取索引值\n",
    "    rights = pred.eq(labels.data.view_as(pred)).sum()  # 预测值是否和真实值是相等的，判断之前需要对真实值做一个view_as(reshape)操作，最后统计数量\n",
    "    return rights, len(labels)  # 返回预测率及真实值长度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 训练网络模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-04-09T14:22:01.525869900Z",
     "start_time": "2024-04-09T14:20:26.766563200Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "当前epoch: 0 [0/60000 (0%)]\t损失: 2.302818\t训练集准确率: 12.50%\t测试集正确率: 13.83%\n",
      "当前epoch: 0 [6400/60000 (11%)]\t损失: 0.172376\t训练集准确率: 79.21%\t测试集正确率: 94.20%\n",
      "当前epoch: 0 [12800/60000 (21%)]\t损失: 0.065588\t训练集准确率: 87.14%\t测试集正确率: 96.34%\n",
      "当前epoch: 0 [19200/60000 (32%)]\t损失: 0.198098\t训练集准确率: 90.16%\t测试集正确率: 97.54%\n",
      "当前epoch: 0 [25600/60000 (43%)]\t损失: 0.032415\t训练集准确率: 91.92%\t测试集正确率: 97.56%\n",
      "当前epoch: 0 [32000/60000 (53%)]\t损失: 0.071576\t训练集准确率: 92.98%\t测试集正确率: 97.89%\n",
      "当前epoch: 0 [38400/60000 (64%)]\t损失: 0.016707\t训练集准确率: 93.84%\t测试集正确率: 98.31%\n",
      "当前epoch: 0 [44800/60000 (75%)]\t损失: 0.081556\t训练集准确率: 94.48%\t测试集正确率: 98.68%\n",
      "当前epoch: 0 [51200/60000 (85%)]\t损失: 0.091402\t训练集准确率: 94.94%\t测试集正确率: 98.39%\n",
      "当前epoch: 0 [57600/60000 (96%)]\t损失: 0.029574\t训练集准确率: 95.28%\t测试集正确率: 97.79%\n",
      "当前epoch: 1 [0/60000 (0%)]\t损失: 0.070397\t训练集准确率: 96.88%\t测试集正确率: 98.67%\n",
      "当前epoch: 1 [6400/60000 (11%)]\t损失: 0.009513\t训练集准确率: 98.75%\t测试集正确率: 98.89%\n",
      "当前epoch: 1 [12800/60000 (21%)]\t损失: 0.004349\t训练集准确率: 98.62%\t测试集正确率: 98.47%\n",
      "当前epoch: 1 [19200/60000 (32%)]\t损失: 0.004965\t训练集准确率: 98.71%\t测试集正确率: 98.82%\n",
      "当前epoch: 1 [25600/60000 (43%)]\t损失: 0.019598\t训练集准确率: 98.72%\t测试集正确率: 98.62%\n",
      "当前epoch: 1 [32000/60000 (53%)]\t损失: 0.134535\t训练集准确率: 98.71%\t测试集正确率: 98.59%\n",
      "当前epoch: 1 [38400/60000 (64%)]\t损失: 0.030853\t训练集准确率: 98.71%\t测试集正确率: 98.80%\n",
      "当前epoch: 1 [44800/60000 (75%)]\t损失: 0.026825\t训练集准确率: 98.71%\t测试集正确率: 98.88%\n",
      "当前epoch: 1 [51200/60000 (85%)]\t损失: 0.056736\t训练集准确率: 98.68%\t测试集正确率: 98.76%\n",
      "当前epoch: 1 [57600/60000 (96%)]\t损失: 0.012040\t训练集准确率: 98.67%\t测试集正确率: 98.78%\n",
      "当前epoch: 2 [0/60000 (0%)]\t损失: 0.071365\t训练集准确率: 98.44%\t测试集正确率: 98.93%\n",
      "当前epoch: 2 [6400/60000 (11%)]\t损失: 0.067400\t训练集准确率: 99.12%\t测试集正确率: 98.99%\n",
      "当前epoch: 2 [12800/60000 (21%)]\t损失: 0.020093\t训练集准确率: 99.13%\t测试集正确率: 98.94%\n",
      "当前epoch: 2 [19200/60000 (32%)]\t损失: 0.004167\t训练集准确率: 99.17%\t测试集正确率: 98.97%\n",
      "当前epoch: 2 [25600/60000 (43%)]\t损失: 0.016776\t训练集准确率: 99.11%\t测试集正确率: 99.07%\n",
      "当前epoch: 2 [32000/60000 (53%)]\t损失: 0.021065\t训练集准确率: 99.08%\t测试集正确率: 98.95%\n",
      "当前epoch: 2 [38400/60000 (64%)]\t损失: 0.008452\t训练集准确率: 99.08%\t测试集正确率: 98.81%\n",
      "当前epoch: 2 [44800/60000 (75%)]\t损失: 0.061853\t训练集准确率: 99.10%\t测试集正确率: 99.14%\n",
      "当前epoch: 2 [51200/60000 (85%)]\t损失: 0.046181\t训练集准确率: 99.11%\t测试集正确率: 99.06%\n",
      "当前epoch: 2 [57600/60000 (96%)]\t损失: 0.010945\t训练集准确率: 99.12%\t测试集正确率: 98.91%\n"
     ]
    }
   ],
   "source": [
    "# 实例化\n",
    "net = CNN()\n",
    "# 损失函数\n",
    "criterion = nn.CrossEntropyLoss()  # 以后都使用CrossEntropyLoss计算损失值而不使用F\n",
    "# 优化器\n",
    "optimizer = optim.Adam(net.parameters(), lr=0.001)  # 定义优化器，普通的随机梯度下降算法(网络中的所有参数, 学习率)\n",
    "\n",
    "# 开始训练循环\n",
    "for epoch in range(num_epochs):\n",
    "    # 当前epoch的结果保存下来\n",
    "    train_rights = []\n",
    "\n",
    "    for batch_idx, (data, target) in enumerate(train_loader):  # 针对容器中的每一个批进行循环\n",
    "        net.train()  # 指定训练模式\n",
    "        output = net(data)  # 得到预测值\n",
    "        loss = criterion(output, target)  # 计算损失\n",
    "        optimizer.zero_grad()  # 梯度清零\n",
    "        loss.backward()  # 反向传播\n",
    "        optimizer.step()  # 参数更新\n",
    "        right = accuracy(output, target)  # 计算预测对了多少个\n",
    "        train_rights.append(right)  # 记录当前batch正确个数\n",
    "\n",
    "        if batch_idx % 100 == 0:  # 间隔指定次数才跑一次验证集\n",
    "            net.eval()  # 验证模式\n",
    "            val_rights = []\n",
    "\n",
    "            for data, target in test_loader:\n",
    "                output = net(data)\n",
    "                right = accuracy(output, target)\n",
    "                val_rights.append(right)\n",
    "\n",
    "            #准确率计算\n",
    "            train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))\n",
    "            val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))\n",
    "\n",
    "            print('当前epoch: {} [{}/{} ({:.0f}%)]\\t损失: {:.6f}\\t训练集准确率: {:.2f}%\\t测试集正确率: {:.2f}%'.format(\n",
    "                epoch,\n",
    "                batch_idx * batch_size,\n",
    "                len(train_loader.dataset),\n",
    "                100. * batch_idx / len(train_loader),\n",
    "                loss.data,\n",
    "                100. * train_r[0].numpy() / train_r[1],\n",
    "                100. * val_r[0].numpy() / val_r[1])\n",
    "            )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "### 练习\n",
    "- 再加入一层卷积，效果怎么样？\n",
    "- 当前任务中为什么全连接层是 32 x 7 x 7 其中每一个数字代表什么含义"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
